rm(list = ls())

In this study, we need the following packages

library(tidyverse)
library(glmnet)
library(leaps)
library(randomForest)
theme_set(theme_bw())

1. Description of the pratical problem

Mashable is a global, multi-platform media and entertainment company. The following dataset summarizes a heterogeneous set of features about articles published by Mashable in a period of two years. The database can be found at the folowing address: https://archive.ics.uci.edu/ml/datasets/online+news+popularity

We read the data.

mydata = read.csv(file = "OnlineNewsPopularity.csv", header=TRUE, sep=",", na.strings = "NA")
head(mydata, n = 1)
dim(mydata)
[1] 39644    61

Our data set is quite large. We have 39644 observations and 61 variables. The goal is to predict the number of shares on social networks (popularity).

2. Description of the mathematical problem

The problem is to explain the output that will be named “nb_shares” by the other variables of the dataset (we will exclude some of them). We denote by \(Y\) the dependent variable “nb_shares” and by \(X_n\) the n independent variables. We want to explain the connection between \(Y\) and \(X_1 ,..., X_n\).

\(Y\) being continuous, we are facing a regression problem. We want to find a machine that will minimize the error between one prediction of our model \(y'\) and one observation \(Y\). In order to measure this error, we will use the quadratic risk function for the machine \(m\) that is \(R(m) = E[(Y-m(X))^2]\) to try to find a machine \(m\) such as \(m_n(x) \approx m^\star(x)\).

As we will see later, we will estimate these risks by splitting our dataset into a train dataset and test dataset.

3. Preparation and cleaning of the data

Cleaning of the names of the columns

  • Transform lower cases
colnames(mydata) = tolower(colnames(mydata))
  • Change the columns names to have more explicit names
colnames(mydata)[colnames(mydata)=="n_tokens_title"] = "nb_words_title"
colnames(mydata)[colnames(mydata)=="n_tokens_content"] = "nb_words_content"
colnames(mydata)[colnames(mydata)=="n_unique_tokens"] = "rate_unique_words"
colnames(mydata)[colnames(mydata)=="n_non_stop_words"] = "rate_non_stop_words"
colnames(mydata)[colnames(mydata)=="n_non_stop_unique_tokens"] = "rate_non_stop_unique_words"
colnames(mydata)[colnames(mydata)=="num_hrefs"] = "nb_links"
# Number of links to other articles published by Mashable
colnames(mydata)[colnames(mydata)=="num_self_hrefs"] = "nb_links_mashable" 
colnames(mydata)[colnames(mydata)=="num_imgs"] = "nb_images"
colnames(mydata)[colnames(mydata)=="num_videos"] = "nb_videos"
colnames(mydata)[colnames(mydata)=="average_token_length"] = "average_word_length"
colnames(mydata)[colnames(mydata)=="num_keywords"] = "nb_keywords"
colnames(mydata)[colnames(mydata)=="shares"] = "nb_shares"  # Target variable
  • Cleaning the data
    We have ratio superior to 1. It is not possible. We find the row where the ratio of rate_non_stop_words is maximum.
index_row = which.max(mydata$rate_non_stop_words)
mydata[index_row,]

A lot of aberrant values. We drop this row.

mydata = mydata[-index_row,]

We also detect an article with 0 words. We check it.

index_row = which.min(mydata$nb_words_content)
mydata[index_row,]

This is a mistake there are words in this article. We exclude the rows without words.

dim(mydata) # Before exclusion
[1] 39643    61
mydata = mydata %>% filter(nb_words_content!=0)
dim(mydata) # After exclusion
[1] 38462    61

We drop the column rate_non_stop_words with only 1 in values.

mydata = mydata %>% select(-rate_non_stop_words)
  • NA values
    We check that we do not have NA values. Look at the number by column.
na_columns = sapply(mydata, function(x) sum(is.na(x)))
na_columns[na_columns>0]
named integer(0)

We do not have any missing values.

  • Creation of categorical variables
    0 - Create an ID by row
number_rows = dim(mydata)[1]
ID = data.frame(ID = c(1:number_rows))
mydata = cbind(ID,mydata)

Transforming the categorical variables to factor.

columns_weekdays = mydata %>% select(contains("weekday")) %>% colnames
columns_channel = mydata %>% select(contains("data_channel_is")) %>% colnames
other_columns = c("is_weekend")
# All the columns names in one vector
columns_to_factor = c(columns_weekdays,columns_channel,other_columns)
# We change them to factor.
mydata[columns_to_factor] = lapply(mydata[columns_to_factor], factor)
  • Group the days and the channel in two variables.
    We have 7 categorical variables: weekday_is_monday and so on. We create a new categorical variable “weekday” with the name of the day.
df_weekday = mydata %>% select(ID, columns_weekdays) %>% gather(key=weekday, value=value_to_filter, -ID)
df_weekday = df_weekday %>% mutate(weekday=recode(weekday,
                                                                         "weekday_is_monday"="monday",
                                                                         "weekday_is_tuesday"="tuesday",
                                                                         "weekday_is_wednesday"="wednesday",
                                                                         "weekday_is_thursday"="thursday",
                                                                         "weekday_is_friday"="friday",
                                                                         "weekday_is_saturday"="saturday",
                                                                         "weekday_is_sunday"="sunday"
                                                                         ))
df_weekday = df_weekday %>% filter(value_to_filter==1)
df_weekday$weekday = as.factor(df_weekday$weekday)
df_weekday$weekday = ordered(df_weekday$weekday, levels=c("monday","tuesday","wednesday","thursday","friday","saturday","sunday"))
df_weekday = df_weekday %>% select(ID, weekday)
mydata = left_join(mydata, df_weekday,by="ID")

We have 6 categorical variables: data_channel_is_lifestyle and so on. We create a new categorical variable “data_channel” with the name of the channel.

df_channel = mydata %>% select(ID, columns_channel) %>% gather(key=channel, value=value_to_filter, -ID)
df_channel = df_channel %>% mutate(channel=recode(channel,
                                                                         "data_channel_is_lifestyle"="lifestyle",
                                                                         "data_channel_is_entertainment"="entertainment",
                                                                         "data_channel_is_bus"="bus",
                                                                         "data_channel_is_socmed"="socmed",
                                                                         "data_channel_is_tech"="tech",
                                                                         "data_channel_is_world"="world"
                                                                         ))
df_channel = df_channel %>% filter(value_to_filter==1)
df_channel$channel = as.factor(df_channel$channel)
levels(df_channel$channel) = c(levels(df_channel$channel),"other")
df_channel = df_channel %>% select(ID, channel)
mydata = left_join(mydata, df_channel,by="ID")
mydata$channel[is.na(mydata$channel)] = "other"
  • Removal of non predictive variables
    We move the target variable to first position after the non predictive variables.
mydata = mydata %>% select(url, ID, timedelta, nb_shares, everything())

To create the models, we create a data.frame “mydata2” without the doublons and the non predictive variables.

mydata2 = mydata %>% select(-c(columns_channel, columns_weekdays,url, ID, timedelta))

4. Description of the dataset (with dplyr)

summary(mydata2)
   nb_shares      nb_words_title  nb_words_content rate_unique_words rate_non_stop_unique_words    nb_links      nb_links_mashable
 Min.   :     1   Min.   : 2.00   Min.   :  18.0   Min.   :0.1150    Min.   :0.1191             Min.   :  0.00   Min.   :  0.000  
 1st Qu.:   945   1st Qu.: 9.00   1st Qu.: 259.0   1st Qu.:0.4774    1st Qu.:0.6326             1st Qu.:  5.00   1st Qu.:  1.000  
 Median :  1400   Median :10.00   Median : 423.0   Median :0.5430    Median :0.6937             Median :  8.00   Median :  3.000  
 Mean   :  3355   Mean   :10.38   Mean   : 563.3   Mean   :0.5468    Mean   :0.6935             Mean   : 11.22   Mean   :  3.395  
 3rd Qu.:  2700   3rd Qu.:12.00   3rd Qu.: 729.0   3rd Qu.:0.6111    3rd Qu.:0.7569             3rd Qu.: 14.00   3rd Qu.:  4.000  
 Max.   :843300   Max.   :23.00   Max.   :8474.0   Max.   :1.0000    Max.   :1.0000             Max.   :304.00   Max.   :116.000  
                                                                                                                                  
   nb_images         nb_videos      average_word_length  nb_keywords       kw_min_min       kw_max_min       kw_avg_min     
 Min.   :  0.000   Min.   : 0.000   Min.   :3.600       Min.   : 1.000   Min.   : -1.00   Min.   :     0   Min.   :   -1.0  
 1st Qu.:  1.000   1st Qu.: 0.000   1st Qu.:4.496       1st Qu.: 6.000   1st Qu.: -1.00   1st Qu.:   445   1st Qu.:  143.0  
 Median :  1.000   Median : 0.000   Median :4.674       Median : 7.000   Median : -1.00   Median :   660   Median :  237.6  
 Mean   :  4.562   Mean   : 1.264   Mean   :4.688       Mean   : 7.215   Mean   : 26.71   Mean   :  1152   Mean   :  313.9  
 3rd Qu.:  4.000   3rd Qu.: 1.000   3rd Qu.:4.862       3rd Qu.: 9.000   3rd Qu.:  4.00   3rd Qu.:  1000   3rd Qu.:  359.2  
 Max.   :128.000   Max.   :91.000   Max.   :8.042       Max.   :10.000   Max.   :377.00   Max.   :298400   Max.   :42827.9  
                                                                                                                            
   kw_min_max       kw_max_max       kw_avg_max       kw_min_avg     kw_max_avg       kw_avg_avg    self_reference_min_shares
 Min.   :     0   Min.   :     0   Min.   :     0   Min.   :  -1   Min.   :     0   Min.   :    0   Min.   :     0           
 1st Qu.:     0   1st Qu.:843300   1st Qu.:171300   1st Qu.:   0   1st Qu.:  3549   1st Qu.: 2374   1st Qu.:   703           
 Median :  1400   Median :843300   Median :242080   Median :1009   Median :  4312   Median : 2851   Median :  1200           
 Mean   : 13182   Mean   :750315   Mean   :255213   Mean   :1102   Mean   :  5604   Mean   : 3103   Mean   :  4122           
 3rd Qu.:  7700   3rd Qu.:843300   3rd Qu.:326864   3rd Qu.:2031   3rd Qu.:  5962   3rd Qu.: 3551   3rd Qu.:  2700           
 Max.   :843300   Max.   :843300   Max.   :843300   Max.   :3613   Max.   :298400   Max.   :43568   Max.   :843300           
                                                                                                                             
 self_reference_max_shares self_reference_avg_sharess is_weekend     lda_00            lda_01            lda_02       
 Min.   :     0            Min.   :     0             0:33436    Min.   :0.01818   Min.   :0.01818   Min.   :0.01818  
 1st Qu.:  1200            1st Qu.:  1100             1: 5026    1st Qu.:0.02506   1st Qu.:0.02501   1st Qu.:0.02857  
 Median :  3000            Median :  2300                        Median :0.03342   Median :0.03335   Median :0.04001  
 Mean   : 10647            Mean   :  6598                        Mean   :0.18814   Mean   :0.14168   Mean   :0.21718  
 3rd Qu.:  8200            3rd Qu.:  5300                        3rd Qu.:0.25195   3rd Qu.:0.15069   3rd Qu.:0.33502  
 Max.   :843300            Max.   :843300                        Max.   :0.92699   Max.   :0.92595   Max.   :0.92000  
                                                                                                                      
     lda_03            lda_04        global_subjectivity global_sentiment_polarity global_rate_positive_words
 Min.   :0.01818   Min.   :0.01818   Min.   :0.0000      Min.   :-0.39375          Min.   :0.00000           
 1st Qu.:0.02562   1st Qu.:0.02858   1st Qu.:0.4025      1st Qu.: 0.06439          1st Qu.:0.02947           
 Median :0.04000   Median :0.05000   Median :0.4566      Median : 0.12252          Median :0.03960           
 Mean   :0.21430   Mean   :0.23870   Mean   :0.4570      Mean   : 0.12298          Mean   :0.04084           
 3rd Qu.:0.34052   3rd Qu.:0.41461   3rd Qu.:0.5103      3rd Qu.: 0.17992          3rd Qu.:0.05072           
 Max.   :0.92653   Max.   :0.92719   Max.   :1.0000      Max.   : 0.72784          Max.   :0.15549           
                                                                                                             
 global_rate_negative_words rate_positive_words rate_negative_words avg_positive_polarity min_positive_polarity
 Min.   :0.00000            Min.   :0.0000      Min.   :0.0000      Min.   :0.0000        Min.   :0.00000      
 1st Qu.:0.01018            1st Qu.:0.6129      1st Qu.:0.2000      1st Qu.:0.3119        1st Qu.:0.05000      
 Median :0.01567            Median :0.7143      Median :0.2857      Median :0.3619        Median :0.10000      
 Mean   :0.01712            Mean   :0.7031      Mean   :0.2968      Mean   :0.3647        Mean   :0.09838      
 3rd Qu.:0.02199            3rd Qu.:0.8000      3rd Qu.:0.3871      3rd Qu.:0.4133        3rd Qu.:0.10000      
 Max.   :0.18493            Max.   :1.0000      Max.   :1.0000      Max.   :1.0000        Max.   :1.00000      
                                                                                                               
 max_positive_polarity avg_negative_polarity min_negative_polarity max_negative_polarity title_subjectivity
 Min.   :0.00          Min.   :-1.0000       Min.   :-1.0000       Min.   :-1.0000       Min.   :0.0000    
 1st Qu.:0.60          1st Qu.:-0.3315       1st Qu.:-0.7143       1st Qu.:-0.1250       1st Qu.:0.0000    
 Median :0.80          Median :-0.2577       Median :-0.5000       Median :-0.1000       Median :0.1250    
 Mean   :0.78          Mean   :-0.2675       Mean   :-0.5380       Mean   :-0.1108       Mean   :0.2806    
 3rd Qu.:1.00          3rd Qu.:-0.1934       3rd Qu.:-0.3125       3rd Qu.:-0.0500       3rd Qu.:0.5000    
 Max.   :1.00          Max.   : 0.0000       Max.   : 0.0000       Max.   : 0.0000       Max.   :1.0000    
                                                                                                           
 title_sentiment_polarity abs_title_subjectivity abs_title_sentiment_polarity      weekday              channel    
 Min.   :-1.0000          Min.   :0.0000         Min.   :0.0000               monday   :6471   bus          :6235  
 1st Qu.: 0.0000          1st Qu.:0.1667         1st Qu.:0.0000               tuesday  :7170   entertainment:6855  
 Median : 0.0000          Median :0.5000         Median :0.0000               wednesday:7205   lifestyle    :2077  
 Mean   : 0.0710          Mean   :0.3424         Mean   :0.1549               thursday :7052   socmed       :2311  
 3rd Qu.: 0.1364          3rd Qu.:0.5000         3rd Qu.:0.2500               friday   :5538   tech         :7325  
 Max.   : 1.0000          Max.   :0.5000         Max.   :1.0000               saturday :2369   world        :8168  
                                                                              sunday   :2657   other        :5491  
colnames(mydata2)
 [1] "nb_shares"                    "nb_words_title"               "nb_words_content"             "rate_unique_words"           
 [5] "rate_non_stop_unique_words"   "nb_links"                     "nb_links_mashable"            "nb_images"                   
 [9] "nb_videos"                    "average_word_length"          "nb_keywords"                  "kw_min_min"                  
[13] "kw_max_min"                   "kw_avg_min"                   "kw_min_max"                   "kw_max_max"                  
[17] "kw_avg_max"                   "kw_min_avg"                   "kw_max_avg"                   "kw_avg_avg"                  
[21] "self_reference_min_shares"    "self_reference_max_shares"    "self_reference_avg_sharess"   "is_weekend"                  
[25] "lda_00"                       "lda_01"                       "lda_02"                       "lda_03"                      
[29] "lda_04"                       "global_subjectivity"          "global_sentiment_polarity"    "global_rate_positive_words"  
[33] "global_rate_negative_words"   "rate_positive_words"          "rate_negative_words"          "avg_positive_polarity"       
[37] "min_positive_polarity"        "max_positive_polarity"        "avg_negative_polarity"        "min_negative_polarity"       
[41] "max_negative_polarity"        "title_subjectivity"           "title_sentiment_polarity"     "abs_title_subjectivity"      
[45] "abs_title_sentiment_polarity" "weekday"                      "channel"                     

Columns numeric

columns_numeric <- unlist(lapply(mydata2, is.numeric))  
cor(mydata2[,c("nb_shares","nb_words_title","nb_words_content","nb_images","nb_videos")])
                   nb_shares nb_words_title nb_words_content    nb_images   nb_videos
nb_shares        1.000000000    0.006212729      0.006701789  0.041279165  0.02471476
nb_words_title   0.006212729    1.000000000      0.028162440 -0.006525119  0.05246549
nb_words_content 0.006701789    0.028162440      1.000000000  0.352948886  0.10205617
nb_images        0.041279165   -0.006525119      0.352948886  1.000000000 -0.06657575
nb_videos        0.024714759    0.052465492      0.102056168 -0.066575748  1.00000000

The correlations are lower that we expected ! Having a lot of content in an article does not make it always popular.

mydata2%>%select(Length.title=nb_words_title,Nb_Links=nb_links,Subjectivity=global_subjectivity,Positive=max_positive_polarity,Content=nb_words_content)%>%summarise_all(funs(mean))
mydata2%>%select(nb_images,nb_videos,nb_shares)%>%mutate(multimedia_content=nb_images+nb_videos)%>%group_by(multimedia_content)%>%summarise(mean_shares=mean(nb_shares))%>%arrange(desc(mean_shares))%>%filter(mean_shares<10000)%>%head()

Articles with the higher shares mean were article with lots of multimedia content (excluding articles considered as outliers with more than 10 000 shares).

subjectivity=mydata2%>%select("nb_shares","title_subjectivity")%>%filter(nb_shares<20000)
subjectivity$title_subjectivity=as.factor(as.numeric(subjectivity$title_subjectivity>0.75))
subjectivity%>%select(title_subjectivity,nb_shares)%>%group_by(title_subjectivity)%>%summarise(Shares=mean(nb_shares))%>%arrange(desc(Shares))

An article with subjectivity in its title tends to drive slightly more shares in average.

5. Vizualization of the dataset (with ggplot)

ggplot(mydata2) + aes(x=nb_shares) + geom_density(fill="darkblue") + xlim(0,20000)+xlab("Number of shares")+ylab("Density")+ labs(title = "Shares Density", subtitle = "Excluding articles with more than 20000 shares") +
  theme(plot.title = element_text(hjust = 0.5,face="bold"),plot.subtitle = element_text(hjust = 0.5))

We observe that most of the articles have a number of shares less than 5000.

ggplot(mydata2) + aes(x=weekday, y=nb_shares) + geom_boxplot() + ylim(0, 10000)+xlab("Weekdays")+ylab("Number of Shares")+ labs(title = "Which day drive more shares?", subtitle = "Excluding articles with more than 10000 shares") +
  theme(plot.title = element_text(hjust = 0.5,face="bold"),plot.subtitle = element_text(hjust = 0.5))

We can observe that Saturday and Sunday drive more shares in average than the other weekdays.

ggplot(mydata2) + aes(x=channel, y=nb_shares) + geom_boxplot() + ylim(0, 10000)+xlab("Channel")+ylab("Number of Shares")+ labs(title = "Which channel drive more shares?", subtitle = "Excluding articles with more than 10000 shares") +
  theme(plot.title = element_text(hjust = 0.5,face="bold"),plot.subtitle = element_text(hjust = 0.5))

The above boxplots show that lifestyle, socmed, tech and other are the four channels that drive in average more shares as we have seen in the description of data.

ggplot(mydata2) + aes(x=weekday,fill=channel)+ geom_bar(position = "stack")+scale_fill_brewer(palette = "Blues")+xlab("Weekdays")+ylab("Frequency")+ labs(title = "Overview by day - Frequency of article posted", subtitle = "Detailed by channel") +
  theme(plot.title = element_text(hjust = 0.5,face="bold"),plot.subtitle = element_text(hjust = 0.5))

We mentionned before that during weekend, articles that are posted drive more shares. Even though, we notice that there is less articles posted compared to other weekdays.

mydata2_temp = sample_n(tbl = mydata2, size = 1000)
ggplot(mydata2_temp) + aes(x=nb_words_content,y=nb_shares)+ geom_point(color="darkblue") +
  geom_smooth(method="lm") +
  ylim(0,50000)+ylab("Number of shares")+xlab("Number of words in the article") + 
  labs(title = "Is there a link between shares and word content?", subtitle = "Excluding articles with more than 50000 shares") +
  theme(plot.title = element_text(hjust = 0.5,face="bold"),plot.subtitle = element_text(hjust = 0.5))

The above graph suggests that the linear relation is week. However we can see that most of the very shared articles do not have a lot of words.

6. Machine Learning Methods

We propose to use the following methods to predict the number of shares:
1 - Linear model
2 - Penalized Regression (Lasso and Ridge)
3 - Random Forest

We create a copy of that dataset that will be used for models

data <- mydata2

Additional preliminary cleaning

There are linear dependencies in the data that will prevent us from running some models :
rate_positive_words + rate_negative_words = 1

data %>% mutate(word_feeling = rate_positive_words + rate_negative_words) %>% ggplot() + aes(x = word_feeling) + geom_density() + ylab("Density")+xlab("Number of words in the articles") + 
  labs(title = "Density of the sum of the variable word_feeling") +  theme(plot.title = element_text(hjust = 0.5,face="bold"))

lda_00 + lda_01 + lda_02 + lda_03 + lda_04 = 1

data %>% mutate(sum_lda = lda_00 + lda_01 + lda_02 + lda_03 + lda_04) %>% ggplot() + aes(x = sum_lda) + geom_density() + ylab("Density")+xlab("Number of words in the articles") + 
  labs(title = "Density of the sum of the variable word_feeling") +  theme(plot.title = element_text(hjust = 0.5,face="bold"))

summary(data$lda_00 + data$lda_01 + data$lda_02 + data$lda_03 + data$lda_04)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
      1       1       1       1       1       1 

In absciss, we have values of 1 due to error approximation in the sum. If we had truncated the values, it would have been only a peak at 1.
We exclude the two problematic variables.

data <- data %>% select(-c(lda_04,rate_negative_words))

Linear Model

We will try to predict the number of shares based on all other regressors We start naively by trying to fit a Linear Regression to the whole dataset

# model.lm <- lm(nb_shares~.,data=data)
# Error: cannot allocate vector of size 11.0 Gb

R cannot compute it, due to the excess of parameters We will therefore : - work only on a sample of the dataset - consider numerical values only

Name_numeric_colums <- rownames(data.frame(columns_numeric[columns_numeric]))
Name_numeric_colums <- Name_numeric_colums[Name_numeric_colums != "lda_04" & Name_numeric_colums != "rate_negative_words"]
  
set.seed(123456)
data <- data %>% sample_n(10000) %>% select(Name_numeric_colums)

We can now separate in training and testing datasets

data.train <- data %>% slice(1:8000)
data.test <- data %>% slice(8001:10000)

We can now run a naive prediction model (that predicts the sample mean of the target column in the train data) and a linear model

pred.naive <- rep(mean(data.train$nb_shares),nrow(data.test))
model.lm <- lm(nb_shares~.,data=data.train)

We do a subset selection. We select the BIC criteria rather than the AIC criteria because it penalises more aggressively the large models. Indeed, compared to AIC, BIC increases by \(log(n)d\) whereas AIC increases by \(2d\). For n = 10000, we will have \(log(n)d \approx9.21\) which is superior to \(2\) of the AIC.

reg.fit.forward <- regsubsets(nb_shares~.,data=data.train,method="forward",nvmax=150)
reg.fit.backward <- regsubsets(nb_shares~.,data=data.train,method="backward",nvmax=150)
sum.reg.fit.f <- summary(reg.fit.forward)
sum.reg.fit.b <- summary(reg.fit.backward)
nb.bic.f <- order(sum.reg.fit.f$bic)[1]
nb.bic.b <- order(sum.reg.fit.b$bic)[1]
get.formula<- function(a,number){
var.sel <- a$which[number,][-1]
var.sel1 <- names(var.sel)[var.sel] %>% paste(collapse="+")
form <- formula(paste("nb_shares~",var.sel1,sep=""))
str_form <- paste("nb_shares~",var.sel1,sep="")
str_form
}
plot(reg.fit.forward,scale="bic",main="Forward BIC")

plot(reg.fit.backward,scale="bic",main="Backward BIC ")

subset.models <- data.frame(Criteria=rep(c("Bic"),each=2),Method=c("Forward","Backward"),Formula = c(get.formula(sum.reg.fit.f,nb.bic.f),get.formula(sum.reg.fit.b,nb.bic.b)))
subset.models

We see that the variables selected with the forward or backward approaches are approximately the same.
For the forward approch, the BIC criteria selects the model: \(Y = \beta_0 + nb.links*\beta_{nb.links}+kw.max.max*\beta_{kw.max.max}+kw.min.avg*\beta_{kw.min.avg}\)
\(+kw.max.avg*\beta_{kw.max.avg}+kw.avg.avg*\beta_{kw.avg.avg}+self.reference.max.shares*\beta_{self.reference.max.shares}+lda.03*\beta_{lda.03}\)

We evaluate those models based on the RMSE

RMSE.list <- c()
RMSE.list <- append(RMSE.list, mean((pred.naive-data.test$nb_shares)^2)^(1/2))
RMSE.list <- append(RMSE.list, mean((predict(model.lm, newdata=data.test)-data.test$nb_shares)^2)^(1/2))
for (formula in subset.models$Formula) {
  model.sub <- lm(formula,data=data.train) 
  pred <- predict(model.sub, newdata = data.test) 
  RMSE <- mean((pred-data.test$nb_shares)^2)^(1/2) 
  RMSE.list <- append(RMSE.list,RMSE) 
  } 
RMSE.comparison <- data.frame(Criteria="mean",Method="Naive",Formula="Y") 
RMSE.comparison <- rbind(RMSE.comparison,data.frame(Criteria="LM",Method="LM",Formula="Y"),subset.models) 
RMSE.comparison <- RMSE.comparison %>% select(Criteria,Method) %>% mutate(RMSE=RMSE.list) 
RMSE.comparison

Penalized regression

data.glmnet <- model.matrix(nb_shares~.,data=data.train)
model.ridge <- glmnet(data.glmnet,data.train$nb_shares,alpha=0)
model.lasso <- glmnet(data.glmnet,data.train$nb_shares,alpha=1)
plot(model.ridge)

plot(model.lasso)

We can observe the regularization path for Lasso and Ridge Regression. For Lasso, some \(\beta=0\) for small \(t\).

ridgeCV <- cv.glmnet(data.glmnet,data.train$nb_shares,lambda=exp(seq(-20,80,length=300)),alpha=0)
plot(ridgeCV)

lassoCV <- cv.glmnet(data.glmnet,data.train$nb_shares,lambda=exp(seq(-10,20,length=300)),alpha=1)
plot(lassoCV)

We look at the 2 best models

model.ridge <- glmnet(data.glmnet,data.train$nb_shares,lambda=ridgeCV$lambda.min,alpha=0)
model.lasso <- glmnet(data.glmnet,data.train$nb_shares,lambda=lassoCV$lambda.min,alpha=1)
RMSE.ridge <- mean((predict(model.ridge, newx=model.matrix(nb_shares~.,data=data.test))-data.test$nb_shares)^2)^(1/2)
RMSE.lasso <- mean((predict(model.lasso, newx=model.matrix(nb_shares~.,data=data.test))-data.test$nb_shares)^2)^(1/2)
RMSE.penalized <- data.frame(Criteria=paste("Lambda = ",ridgeCV$lambda.min),Method="Ridge",RMSE=RMSE.ridge)
RMSE.penalized <- rbind(RMSE.penalized, data.frame(Criteria=paste("Lambda = ",lassoCV$lambda.min),Method="Lasso",RMSE=RMSE.lasso))
RMSE.comparison <- rbind(RMSE.comparison, RMSE.penalized)
RMSE.comparison

Random Forests

We reduce the number and size of tree to make it compute in a timely manner

model.rf <- randomForest(nb_shares~.,data=data.train,nodesize=30,ntree=300)
model.rf

Call:
 randomForest(formula = nb_shares ~ ., data = data.train, nodesize = 30,      ntree = 300) 
               Type of random forest: regression
                     Number of trees: 300
No. of variables tried at each split: 13

          Mean of squared residuals: 59282165
                    % Var explained: -0.17
RMSE.rf <- mean((predict(model.rf, newdata=data.test)-data.test$nb_shares)^2)^(1/2)
RMSE.comparison <- rbind(RMSE.comparison, data.frame(Criteria="Node size = 30, ntree = 300",Method="RF",RMSE=RMSE.rf))
RMSE.comparison

We increase parameters to get a more accurate prediction

model.rf2 <- randomForest(nb_shares~.,data=data.train,nodesize=1,ntree=400)
RMSE.rf <- mean((predict(model.rf2, newdata=data.test)-data.test$nb_shares)^2)^(1/2)
RMSE.comparison <- rbind(RMSE.comparison, data.frame(Criteria="Node size = 1, ntree = 400",Method="RF",RMSE=RMSE.rf))
RMSE.comparison

It doesn’t improve the prediction, quite the contrary

7. Performances of each models

Based on our table of comparison of RMSE for each models, Lasso is the best performing model

pred <- predict(model.lasso, newx=model.matrix(nb_shares~.,data=data.test))
obs <- data.test$nb_shares
res <- obs-pred
Prediction <- data.frame(pred, obs,res)
colnames(Prediction) <- c("pred","obs","res")
Prediction %>% ggplot() + aes(x=res) + geom_density()

Our model is quite optimistic (peak before 0 in residual’s distribution), but still misses the few viral articles that get shared a lot. These are the ones that have a major impact on the poor RMSE score

Prediction <- Prediction %>% mutate(ratio = res/obs)
Prediction %>% ggplot() + aes(x=ratio) + geom_density()

Prediction %>% ggplot() + aes(x=pred, y=obs, color=res) + geom_point()

We can see very few articles reach more than 12500 shares. They get very poor predictions as the model never predicts more than 11000 shares, and have a significant impact on the overall RMSE

Pred.trim <- Prediction[Prediction$obs < 12500,]
Pred.trim %>% ggplot() + aes(x=pred, y=obs, color=res) + geom_point()

mean(Pred.trim$res^2)^(1/2)
[1] 2372.71

If we had only observations with less than 12500 shares in the test data, the RMSE could have had been halfed. If on top of that we would not have viral articles in the train data, the RMSE would have been improved. However, because our goal was to predict the number of shares for all articles (including the ones with more than 12500 shares), we did not exclude popular articles from the database.

8. Conclusion

The popularity of an article depends on many characteristics: day of publication, length, channel, content… Using these characteristics as predictors, we have tried to predict the number of shares on social networks (popularity). Throughout analysis and modelling, we realise that there isn’t a stable and standard recipe that will determine with a strong accuracy the popularity of an article. Our models can give an idea of the success but as Mashable has published articles making major buzz (more than 20 000 shares each; maximum reached with 840k shares), this twists our models and makes them more optimistic that they should be.

Furthermore, we could have transformed the regression problem in a classification problem, for example \(nb.shares>12000\), that could have allow to create more robust model to predict whether an article would turn viral (meaning number of shares superior to 12000) or not. This model could potentially be combined afterwards with two different models dedicated to predicting number of shares for viral and for non-viral articles exclusively.

To finish, we can keep in mind that the classification model is a lead to empower authors in defining what factors could be more impactfull to result in more engagement and virality.

LS0tCnRpdGxlOiAiUHJvamVjdCAtIE9ubGluZSBOZXdzIFBvcHVsYXJpdHkiCm91dHB1dDogaHRtbF9ub3RlYm9vawoKLS0tCmBgYHtyfQpybShsaXN0ID0gbHMoKSkKYGBgCgoKCkluIHRoaXMgc3R1ZHksIHdlIG5lZWQgdGhlIGZvbGxvd2luZyBwYWNrYWdlcyAgCmBgYHtyIGVjaG89VFJVRSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkoZ2xtbmV0KQpsaWJyYXJ5KGxlYXBzKQpsaWJyYXJ5KHJhbmRvbUZvcmVzdCkKdGhlbWVfc2V0KHRoZW1lX2J3KCkpCmBgYAoKCiMjIDEuIERlc2NyaXB0aW9uIG9mIHRoZSBwcmF0aWNhbCBwcm9ibGVtICAKTWFzaGFibGUgaXMgYSBnbG9iYWwsIG11bHRpLXBsYXRmb3JtIG1lZGlhIGFuZCBlbnRlcnRhaW5tZW50IGNvbXBhbnkuIFRoZSBmb2xsb3dpbmcgZGF0YXNldCBzdW1tYXJpemVzIGEgaGV0ZXJvZ2VuZW91cyBzZXQgb2YgZmVhdHVyZXMgYWJvdXQgYXJ0aWNsZXMgcHVibGlzaGVkIGJ5IE1hc2hhYmxlIGluIGEgcGVyaW9kIG9mIHR3byB5ZWFycy4gClRoZSBkYXRhYmFzZSBjYW4gYmUgZm91bmQgYXQgdGhlIGZvbG93aW5nIGFkZHJlc3M6IGh0dHBzOi8vYXJjaGl2ZS5pY3MudWNpLmVkdS9tbC9kYXRhc2V0cy9vbmxpbmUrbmV3cytwb3B1bGFyaXR5CgpXZSByZWFkIHRoZSBkYXRhLgpgYGB7cn0KbXlkYXRhID0gcmVhZC5jc3YoZmlsZSA9ICJPbmxpbmVOZXdzUG9wdWxhcml0eS5jc3YiLCBoZWFkZXI9VFJVRSwgc2VwPSIsIiwgbmEuc3RyaW5ncyA9ICJOQSIpCmhlYWQobXlkYXRhLCBuID0gMSkKYGBgCgpgYGB7cn0KZGltKG15ZGF0YSkKYGBgCk91ciBkYXRhIHNldCBpcyBxdWl0ZSBsYXJnZS4gV2UgaGF2ZSAzOTY0NCBvYnNlcnZhdGlvbnMgYW5kIDYxIHZhcmlhYmxlcy4KVGhlIGdvYWwgaXMgdG8gcHJlZGljdCB0aGUgbnVtYmVyIG9mIHNoYXJlcyBvbiBzb2NpYWwgbmV0d29ya3MgKHBvcHVsYXJpdHkpLgoKIyMgMi4gRGVzY3JpcHRpb24gb2YgdGhlIG1hdGhlbWF0aWNhbCBwcm9ibGVtClRoZSBwcm9ibGVtIGlzIHRvIGV4cGxhaW4gdGhlIG91dHB1dCB0aGF0IHdpbGwgYmUgbmFtZWQgIm5iX3NoYXJlcyIgYnkgdGhlIG90aGVyIHZhcmlhYmxlcyBvZiB0aGUgZGF0YXNldCAod2Ugd2lsbCBleGNsdWRlIHNvbWUgb2YgdGhlbSkuIFdlIGRlbm90ZSBieSAkWSQgdGhlIGRlcGVuZGVudCB2YXJpYWJsZSAibmJfc2hhcmVzIiBhbmQgYnkgJFhfbiQgdGhlIG4gaW5kZXBlbmRlbnQgdmFyaWFibGVzLiBXZSB3YW50IHRvIGV4cGxhaW4gdGhlIGNvbm5lY3Rpb24gYmV0d2VlbiAkWSQgYW5kICRYXzEgLC4uLiwgWF9uJC4KCiRZJCAgYmVpbmcgY29udGludW91cywgd2UgYXJlIGZhY2luZyBhIHJlZ3Jlc3Npb24gcHJvYmxlbS4gV2Ugd2FudCB0byBmaW5kIGEgbWFjaGluZSB0aGF0IHdpbGwgbWluaW1pemUgdGhlIGVycm9yIGJldHdlZW4gb25lIHByZWRpY3Rpb24gb2Ygb3VyIG1vZGVsICR5JyQgYW5kIG9uZSBvYnNlcnZhdGlvbiAkWSQuIEluIG9yZGVyIHRvIG1lYXN1cmUgdGhpcyBlcnJvciwgd2Ugd2lsbCB1c2UgdGhlICoqcXVhZHJhdGljIHJpc2sgZnVuY3Rpb24qKiBmb3IgdGhlIG1hY2hpbmUgJG0kIHRoYXQgaXMgJFIobSkgPSBFWyhZLW0oWCkpXjJdJCB0byB0cnkgdG8gZmluZCBhIG1hY2hpbmUgJG0kIHN1Y2ggYXMgJG1fbih4KSBcYXBwcm94IG1eXHN0YXIoeCkkLgoKQXMgd2Ugd2lsbCBzZWUgbGF0ZXIsIHdlIHdpbGwgZXN0aW1hdGUgdGhlc2Ugcmlza3MgYnkgc3BsaXR0aW5nIG91ciBkYXRhc2V0IGludG8gYSAqKnRyYWluKiogZGF0YXNldCBhbmQgKip0ZXN0KiogZGF0YXNldC4KCgojIyAzLiBQcmVwYXJhdGlvbiBhbmQgY2xlYW5pbmcgb2YgdGhlIGRhdGEKIyMjIENsZWFuaW5nIG9mIHRoZSBuYW1lcyBvZiB0aGUgY29sdW1ucwoKKiAqKlRyYW5zZm9ybSBsb3dlciBjYXNlcyoqCmBgYHtyfQpjb2xuYW1lcyhteWRhdGEpID0gdG9sb3dlcihjb2xuYW1lcyhteWRhdGEpKQpgYGAKCiogKipDaGFuZ2UgdGhlIGNvbHVtbnMgbmFtZXMgdG8gaGF2ZSBtb3JlIGV4cGxpY2l0IG5hbWVzKioKYGBge3J9CmNvbG5hbWVzKG15ZGF0YSlbY29sbmFtZXMobXlkYXRhKT09Im5fdG9rZW5zX3RpdGxlIl0gPSAibmJfd29yZHNfdGl0bGUiCmNvbG5hbWVzKG15ZGF0YSlbY29sbmFtZXMobXlkYXRhKT09Im5fdG9rZW5zX2NvbnRlbnQiXSA9ICJuYl93b3Jkc19jb250ZW50Igpjb2xuYW1lcyhteWRhdGEpW2NvbG5hbWVzKG15ZGF0YSk9PSJuX3VuaXF1ZV90b2tlbnMiXSA9ICJyYXRlX3VuaXF1ZV93b3JkcyIKY29sbmFtZXMobXlkYXRhKVtjb2xuYW1lcyhteWRhdGEpPT0ibl9ub25fc3RvcF93b3JkcyJdID0gInJhdGVfbm9uX3N0b3Bfd29yZHMiCmNvbG5hbWVzKG15ZGF0YSlbY29sbmFtZXMobXlkYXRhKT09Im5fbm9uX3N0b3BfdW5pcXVlX3Rva2VucyJdID0gInJhdGVfbm9uX3N0b3BfdW5pcXVlX3dvcmRzIgpjb2xuYW1lcyhteWRhdGEpW2NvbG5hbWVzKG15ZGF0YSk9PSJudW1faHJlZnMiXSA9ICJuYl9saW5rcyIKCiMgTnVtYmVyIG9mIGxpbmtzIHRvIG90aGVyIGFydGljbGVzIHB1Ymxpc2hlZCBieSBNYXNoYWJsZQpjb2xuYW1lcyhteWRhdGEpW2NvbG5hbWVzKG15ZGF0YSk9PSJudW1fc2VsZl9ocmVmcyJdID0gIm5iX2xpbmtzX21hc2hhYmxlIiAKY29sbmFtZXMobXlkYXRhKVtjb2xuYW1lcyhteWRhdGEpPT0ibnVtX2ltZ3MiXSA9ICJuYl9pbWFnZXMiCmNvbG5hbWVzKG15ZGF0YSlbY29sbmFtZXMobXlkYXRhKT09Im51bV92aWRlb3MiXSA9ICJuYl92aWRlb3MiCmNvbG5hbWVzKG15ZGF0YSlbY29sbmFtZXMobXlkYXRhKT09ImF2ZXJhZ2VfdG9rZW5fbGVuZ3RoIl0gPSAiYXZlcmFnZV93b3JkX2xlbmd0aCIKY29sbmFtZXMobXlkYXRhKVtjb2xuYW1lcyhteWRhdGEpPT0ibnVtX2tleXdvcmRzIl0gPSAibmJfa2V5d29yZHMiCmNvbG5hbWVzKG15ZGF0YSlbY29sbmFtZXMobXlkYXRhKT09InNoYXJlcyJdID0gIm5iX3NoYXJlcyIgICMgVGFyZ2V0IHZhcmlhYmxlCmBgYAoKCiogKipDbGVhbmluZyB0aGUgZGF0YSoqICAKV2UgaGF2ZSByYXRpbyBzdXBlcmlvciB0byAxLiBJdCBpcyBub3QgcG9zc2libGUuIFdlIGZpbmQgdGhlIHJvdyB3aGVyZSB0aGUgcmF0aW8gb2YgcmF0ZV9ub25fc3RvcF93b3JkcyBpcyBtYXhpbXVtLgpgYGB7cn0KaW5kZXhfcm93ID0gd2hpY2gubWF4KG15ZGF0YSRyYXRlX25vbl9zdG9wX3dvcmRzKQpteWRhdGFbaW5kZXhfcm93LF0KYGBgCkEgbG90IG9mIGFiZXJyYW50IHZhbHVlcy4gV2UgZHJvcCB0aGlzIHJvdy4KYGBge3J9Cm15ZGF0YSA9IG15ZGF0YVstaW5kZXhfcm93LF0KYGBgCldlIGFsc28gZGV0ZWN0IGFuIGFydGljbGUgd2l0aCAwIHdvcmRzLiBXZSBjaGVjayBpdC4KYGBge3J9CmluZGV4X3JvdyA9IHdoaWNoLm1pbihteWRhdGEkbmJfd29yZHNfY29udGVudCkKbXlkYXRhW2luZGV4X3JvdyxdCmBgYApUaGlzIGlzIGEgbWlzdGFrZSB0aGVyZSBhcmUgd29yZHMgaW4gdGhpcyBhcnRpY2xlLiBXZSBleGNsdWRlIHRoZSByb3dzIHdpdGhvdXQgd29yZHMuCmBgYHtyfQpkaW0obXlkYXRhKSAjIEJlZm9yZSBleGNsdXNpb24KbXlkYXRhID0gbXlkYXRhICU+JSBmaWx0ZXIobmJfd29yZHNfY29udGVudCE9MCkKZGltKG15ZGF0YSkgIyBBZnRlciBleGNsdXNpb24KYGBgCldlIGRyb3AgdGhlIGNvbHVtbiByYXRlX25vbl9zdG9wX3dvcmRzIHdpdGggb25seSAxIGluIHZhbHVlcy4KYGBge3J9Cm15ZGF0YSA9IG15ZGF0YSAlPiUgc2VsZWN0KC1yYXRlX25vbl9zdG9wX3dvcmRzKQpgYGAKCiogKipOQSB2YWx1ZXMqKiAgCldlIGNoZWNrIHRoYXQgd2UgZG8gbm90IGhhdmUgTkEgdmFsdWVzLiBMb29rIGF0IHRoZSBudW1iZXIgYnkgY29sdW1uLgpgYGB7cn0KbmFfY29sdW1ucyA9IHNhcHBseShteWRhdGEsIGZ1bmN0aW9uKHgpIHN1bShpcy5uYSh4KSkpCm5hX2NvbHVtbnNbbmFfY29sdW1ucz4wXQpgYGAKV2UgZG8gbm90IGhhdmUgYW55IG1pc3NpbmcgdmFsdWVzLgoKKiAqKkNyZWF0aW9uIG9mIGNhdGVnb3JpY2FsIHZhcmlhYmxlcyoqICAKMCAtIENyZWF0ZSBhbiBJRCBieSByb3cKYGBge3J9Cm51bWJlcl9yb3dzID0gZGltKG15ZGF0YSlbMV0KSUQgPSBkYXRhLmZyYW1lKElEID0gYygxOm51bWJlcl9yb3dzKSkKbXlkYXRhID0gY2JpbmQoSUQsbXlkYXRhKQpgYGAKCgoqKlRyYW5zZm9ybWluZyB0aGUgY2F0ZWdvcmljYWwgdmFyaWFibGVzIHRvIGZhY3Rvci4qKgpgYGB7cn0KY29sdW1uc193ZWVrZGF5cyA9IG15ZGF0YSAlPiUgc2VsZWN0KGNvbnRhaW5zKCJ3ZWVrZGF5IikpICU+JSBjb2xuYW1lcwpjb2x1bW5zX2NoYW5uZWwgPSBteWRhdGEgJT4lIHNlbGVjdChjb250YWlucygiZGF0YV9jaGFubmVsX2lzIikpICU+JSBjb2xuYW1lcwpvdGhlcl9jb2x1bW5zID0gYygiaXNfd2Vla2VuZCIpCiMgQWxsIHRoZSBjb2x1bW5zIG5hbWVzIGluIG9uZSB2ZWN0b3IKY29sdW1uc190b19mYWN0b3IgPSBjKGNvbHVtbnNfd2Vla2RheXMsY29sdW1uc19jaGFubmVsLG90aGVyX2NvbHVtbnMpCiMgV2UgY2hhbmdlIHRoZW0gdG8gZmFjdG9yLgpteWRhdGFbY29sdW1uc190b19mYWN0b3JdID0gbGFwcGx5KG15ZGF0YVtjb2x1bW5zX3RvX2ZhY3Rvcl0sIGZhY3RvcikKYGBgCgoqICoqR3JvdXAgdGhlIGRheXMgYW5kIHRoZSBjaGFubmVsIGluIHR3byB2YXJpYWJsZXMuKiogICAKV2UgaGF2ZSA3IGNhdGVnb3JpY2FsIHZhcmlhYmxlczogd2Vla2RheV9pc19tb25kYXkgYW5kIHNvIG9uLiAKV2UgY3JlYXRlIGEgbmV3IGNhdGVnb3JpY2FsIHZhcmlhYmxlICJ3ZWVrZGF5IiB3aXRoIHRoZSBuYW1lIG9mIHRoZSBkYXkuCgpgYGB7cn0KZGZfd2Vla2RheSA9IG15ZGF0YSAlPiUgc2VsZWN0KElELCBjb2x1bW5zX3dlZWtkYXlzKSAlPiUgZ2F0aGVyKGtleT13ZWVrZGF5LCB2YWx1ZT12YWx1ZV90b19maWx0ZXIsIC1JRCkKZGZfd2Vla2RheSA9IGRmX3dlZWtkYXkgJT4lIG11dGF0ZSh3ZWVrZGF5PXJlY29kZSh3ZWVrZGF5LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIndlZWtkYXlfaXNfbW9uZGF5Ij0ibW9uZGF5IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ3ZWVrZGF5X2lzX3R1ZXNkYXkiPSJ0dWVzZGF5IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ3ZWVrZGF5X2lzX3dlZG5lc2RheSI9IndlZG5lc2RheSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAid2Vla2RheV9pc190aHVyc2RheSI9InRodXJzZGF5IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ3ZWVrZGF5X2lzX2ZyaWRheSI9ImZyaWRheSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAid2Vla2RheV9pc19zYXR1cmRheSI9InNhdHVyZGF5IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ3ZWVrZGF5X2lzX3N1bmRheSI9InN1bmRheSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICkpCmRmX3dlZWtkYXkgPSBkZl93ZWVrZGF5ICU+JSBmaWx0ZXIodmFsdWVfdG9fZmlsdGVyPT0xKQpkZl93ZWVrZGF5JHdlZWtkYXkgPSBhcy5mYWN0b3IoZGZfd2Vla2RheSR3ZWVrZGF5KQpkZl93ZWVrZGF5JHdlZWtkYXkgPSBvcmRlcmVkKGRmX3dlZWtkYXkkd2Vla2RheSwgbGV2ZWxzPWMoIm1vbmRheSIsInR1ZXNkYXkiLCJ3ZWRuZXNkYXkiLCJ0aHVyc2RheSIsImZyaWRheSIsInNhdHVyZGF5Iiwic3VuZGF5IikpCmRmX3dlZWtkYXkgPSBkZl93ZWVrZGF5ICU+JSBzZWxlY3QoSUQsIHdlZWtkYXkpCm15ZGF0YSA9IGxlZnRfam9pbihteWRhdGEsIGRmX3dlZWtkYXksYnk9IklEIikKYGBgCgpXZSBoYXZlIDYgY2F0ZWdvcmljYWwgdmFyaWFibGVzOiBkYXRhX2NoYW5uZWxfaXNfbGlmZXN0eWxlIGFuZCBzbyBvbi4gCldlIGNyZWF0ZSBhIG5ldyBjYXRlZ29yaWNhbCB2YXJpYWJsZSAiZGF0YV9jaGFubmVsIiB3aXRoIHRoZSBuYW1lIG9mIHRoZSBjaGFubmVsLgpgYGB7cn0KZGZfY2hhbm5lbCA9IG15ZGF0YSAlPiUgc2VsZWN0KElELCBjb2x1bW5zX2NoYW5uZWwpICU+JSBnYXRoZXIoa2V5PWNoYW5uZWwsIHZhbHVlPXZhbHVlX3RvX2ZpbHRlciwgLUlEKQpkZl9jaGFubmVsID0gZGZfY2hhbm5lbCAlPiUgbXV0YXRlKGNoYW5uZWw9cmVjb2RlKGNoYW5uZWwsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YV9jaGFubmVsX2lzX2xpZmVzdHlsZSI9ImxpZmVzdHlsZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YV9jaGFubmVsX2lzX2VudGVydGFpbm1lbnQiPSJlbnRlcnRhaW5tZW50IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkYXRhX2NoYW5uZWxfaXNfYnVzIj0iYnVzIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkYXRhX2NoYW5uZWxfaXNfc29jbWVkIj0ic29jbWVkIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkYXRhX2NoYW5uZWxfaXNfdGVjaCI9InRlY2giLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImRhdGFfY2hhbm5lbF9pc193b3JsZCI9IndvcmxkIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKSkKZGZfY2hhbm5lbCA9IGRmX2NoYW5uZWwgJT4lIGZpbHRlcih2YWx1ZV90b19maWx0ZXI9PTEpCmRmX2NoYW5uZWwkY2hhbm5lbCA9IGFzLmZhY3RvcihkZl9jaGFubmVsJGNoYW5uZWwpCmxldmVscyhkZl9jaGFubmVsJGNoYW5uZWwpID0gYyhsZXZlbHMoZGZfY2hhbm5lbCRjaGFubmVsKSwib3RoZXIiKQpkZl9jaGFubmVsID0gZGZfY2hhbm5lbCAlPiUgc2VsZWN0KElELCBjaGFubmVsKQpteWRhdGEgPSBsZWZ0X2pvaW4obXlkYXRhLCBkZl9jaGFubmVsLGJ5PSJJRCIpCm15ZGF0YSRjaGFubmVsW2lzLm5hKG15ZGF0YSRjaGFubmVsKV0gPSAib3RoZXIiCmBgYAoKKiAqKlJlbW92YWwgb2Ygbm9uIHByZWRpY3RpdmUgdmFyaWFibGVzKiogIApXZSBtb3ZlIHRoZSB0YXJnZXQgdmFyaWFibGUgdG8gZmlyc3QgcG9zaXRpb24gYWZ0ZXIgdGhlIG5vbiBwcmVkaWN0aXZlIHZhcmlhYmxlcy4KYGBge3J9Cm15ZGF0YSA9IG15ZGF0YSAlPiUgc2VsZWN0KHVybCwgSUQsIHRpbWVkZWx0YSwgbmJfc2hhcmVzLCBldmVyeXRoaW5nKCkpCmBgYAoKVG8gY3JlYXRlIHRoZSBtb2RlbHMsIHdlIGNyZWF0ZSBhIGRhdGEuZnJhbWUgIm15ZGF0YTIiIHdpdGhvdXQgdGhlIGRvdWJsb25zIGFuZCB0aGUgbm9uIHByZWRpY3RpdmUgdmFyaWFibGVzLgpgYGB7cn0KbXlkYXRhMiA9IG15ZGF0YSAlPiUgc2VsZWN0KC1jKGNvbHVtbnNfY2hhbm5lbCwgY29sdW1uc193ZWVrZGF5cyx1cmwsIElELCB0aW1lZGVsdGEpKQpgYGAKCgojIyA0LiBEZXNjcmlwdGlvbiBvZiB0aGUgZGF0YXNldCAod2l0aCBkcGx5cikKCmBgYHtyfQpzdW1tYXJ5KG15ZGF0YTIpCmBgYApgYGB7cn0KY29sbmFtZXMobXlkYXRhMikKYGBgCgpDb2x1bW5zIG51bWVyaWMKYGBge3J9CmNvbHVtbnNfbnVtZXJpYyA8LSB1bmxpc3QobGFwcGx5KG15ZGF0YTIsIGlzLm51bWVyaWMpKSAgCmBgYAoKCiogKipXZSBmb2xsb3cgb3VyIGludHVpdGlvbiBhbmQgbG9vayBhdCBzb21lIGNvcnJlbGF0aW9ucyB0aGF0IGNvdWxkIGJlIGludGVyZXN0aW5nKioKYGBge3J9CmNvcihteWRhdGEyWyxjKCJuYl9zaGFyZXMiLCJuYl93b3Jkc190aXRsZSIsIm5iX3dvcmRzX2NvbnRlbnQiLCJuYl9pbWFnZXMiLCJuYl92aWRlb3MiKV0pCmBgYApUaGUgY29ycmVsYXRpb25zIGFyZSBsb3dlciB0aGF0IHdlIGV4cGVjdGVkICEgSGF2aW5nIGEgbG90IG9mIGNvbnRlbnQgaW4gYW4gYXJ0aWNsZSBkb2VzIG5vdCBtYWtlIGl0IGFsd2F5cyBwb3B1bGFyLgoKKiAqKk92ZXJ2aWV3IC0gTWFpbiBmZWF0dXJlcyBvZiBhbiBhcnRpY2xlICoqCmBgYHtyfQpteWRhdGEyJT4lc2VsZWN0KExlbmd0aC50aXRsZT1uYl93b3Jkc190aXRsZSxOYl9MaW5rcz1uYl9saW5rcyxTdWJqZWN0aXZpdHk9Z2xvYmFsX3N1YmplY3Rpdml0eSxQb3NpdGl2ZT1tYXhfcG9zaXRpdmVfcG9sYXJpdHksQ29udGVudD1uYl93b3Jkc19jb250ZW50KSU+JXN1bW1hcmlzZV9hbGwoZnVucyhtZWFuKSkKYGBgCgoKKiAqKkRvZXMgYWRkaW5nIG11bHRpbWVkaWEgY29udGVudCBoYXZlIGFuIGltcGFjdCBvZiBudW1iZXIgb2Ygc2hhcmVzPyoqCgpgYGB7cn0KbXlkYXRhMiU+JXNlbGVjdChuYl9pbWFnZXMsbmJfdmlkZW9zLG5iX3NoYXJlcyklPiVtdXRhdGUobXVsdGltZWRpYV9jb250ZW50PW5iX2ltYWdlcytuYl92aWRlb3MpJT4lZ3JvdXBfYnkobXVsdGltZWRpYV9jb250ZW50KSU+JXN1bW1hcmlzZShtZWFuX3NoYXJlcz1tZWFuKG5iX3NoYXJlcykpJT4lYXJyYW5nZShkZXNjKG1lYW5fc2hhcmVzKSklPiVmaWx0ZXIobWVhbl9zaGFyZXM8MTAwMDApJT4laGVhZCgpCmBgYApBcnRpY2xlcyB3aXRoIHRoZSBoaWdoZXIgc2hhcmVzIG1lYW4gd2VyZSBhcnRpY2xlIHdpdGggbG90cyBvZiBtdWx0aW1lZGlhIGNvbnRlbnQgKGV4Y2x1ZGluZyBhcnRpY2xlcyBjb25zaWRlcmVkIGFzIG91dGxpZXJzIHdpdGggbW9yZSB0aGFuIDEwIDAwMCBzaGFyZXMpLgoKCiogKipEb2VzIGFuIGFydGljbGUgd2l0aCBhIHN1YmplY3RpdmUgdGl0bGUgZHJpdmUgbW9yZSBzaGFyZXM/KioKYGBge3J9CnN1YmplY3Rpdml0eT1teWRhdGEyJT4lc2VsZWN0KCJuYl9zaGFyZXMiLCJ0aXRsZV9zdWJqZWN0aXZpdHkiKSU+JWZpbHRlcihuYl9zaGFyZXM8MjAwMDApCnN1YmplY3Rpdml0eSR0aXRsZV9zdWJqZWN0aXZpdHk9YXMuZmFjdG9yKGFzLm51bWVyaWMoc3ViamVjdGl2aXR5JHRpdGxlX3N1YmplY3Rpdml0eT4wLjc1KSkKCnN1YmplY3Rpdml0eSU+JXNlbGVjdCh0aXRsZV9zdWJqZWN0aXZpdHksbmJfc2hhcmVzKSU+JWdyb3VwX2J5KHRpdGxlX3N1YmplY3Rpdml0eSklPiVzdW1tYXJpc2UoU2hhcmVzPW1lYW4obmJfc2hhcmVzKSklPiVhcnJhbmdlKGRlc2MoU2hhcmVzKSkKCmBgYAoKQW4gYXJ0aWNsZSB3aXRoIHN1YmplY3Rpdml0eSBpbiBpdHMgdGl0bGUgdGVuZHMgdG8gZHJpdmUgc2xpZ2h0bHkgbW9yZSBzaGFyZXMgaW4gYXZlcmFnZS4KCiMjIDUuIFZpenVhbGl6YXRpb24gb2YgdGhlIGRhdGFzZXQgKHdpdGggZ2dwbG90KQoKKiAqKkhvdyB0aGUgbnVtYmVyIG9mIHNoYXJlcyBpcyBkaXN0cmlidXRlZD8qKgpgYGB7cn0KZ2dwbG90KG15ZGF0YTIpICsgYWVzKHg9bmJfc2hhcmVzKSArIGdlb21fZGVuc2l0eShmaWxsPSJkYXJrYmx1ZSIpICsgeGxpbSgwLDIwMDAwKSt4bGFiKCJOdW1iZXIgb2Ygc2hhcmVzIikreWxhYigiRGVuc2l0eSIpKyBsYWJzKHRpdGxlID0gIlNoYXJlcyBEZW5zaXR5Iiwgc3VidGl0bGUgPSAiRXhjbHVkaW5nIGFydGljbGVzIHdpdGggbW9yZSB0aGFuIDIwMDAwIHNoYXJlcyIpICsKICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41LGZhY2U9ImJvbGQiKSxwbG90LnN1YnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSkKYGBgCldlIG9ic2VydmUgdGhhdCBtb3N0IG9mIHRoZSBhcnRpY2xlcyBoYXZlIGEgbnVtYmVyIG9mIHNoYXJlcyBsZXNzIHRoYW4gNTAwMC4gCgoKCiogKipXaGljaCB3ZWVrZGF5IGRyaXZlIG1vcmUgc2hhcmVzPyoqCmBgYHtyfQpnZ3Bsb3QobXlkYXRhMikgKyBhZXMoeD13ZWVrZGF5LCB5PW5iX3NoYXJlcykgKyBnZW9tX2JveHBsb3QoKSArIHlsaW0oMCwgMTAwMDApK3hsYWIoIldlZWtkYXlzIikreWxhYigiTnVtYmVyIG9mIFNoYXJlcyIpKyBsYWJzKHRpdGxlID0gIldoaWNoIGRheSBkcml2ZSBtb3JlIHNoYXJlcz8iLCBzdWJ0aXRsZSA9ICJFeGNsdWRpbmcgYXJ0aWNsZXMgd2l0aCBtb3JlIHRoYW4gMTAwMDAgc2hhcmVzIikgKwogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUsZmFjZT0iYm9sZCIpLHBsb3Quc3VidGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpKQpgYGAKV2UgY2FuIG9ic2VydmUgdGhhdCBTYXR1cmRheSBhbmQgU3VuZGF5IGRyaXZlIG1vcmUgc2hhcmVzIGluIGF2ZXJhZ2UgdGhhbiB0aGUgb3RoZXIgd2Vla2RheXMuIAoKKiAqKldoaWNoIGNoYW5uZWwgZHJpdmUgbW9yZSBzaGFyZXM/KioKYGBge3J9CmdncGxvdChteWRhdGEyKSArIGFlcyh4PWNoYW5uZWwsIHk9bmJfc2hhcmVzKSArIGdlb21fYm94cGxvdCgpICsgeWxpbSgwLCAxMDAwMCkreGxhYigiQ2hhbm5lbCIpK3lsYWIoIk51bWJlciBvZiBTaGFyZXMiKSsgbGFicyh0aXRsZSA9ICJXaGljaCBjaGFubmVsIGRyaXZlIG1vcmUgc2hhcmVzPyIsIHN1YnRpdGxlID0gIkV4Y2x1ZGluZyBhcnRpY2xlcyB3aXRoIG1vcmUgdGhhbiAxMDAwMCBzaGFyZXMiKSArCiAgdGhlbWUocGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSxmYWNlPSJib2xkIikscGxvdC5zdWJ0aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSkpCmBgYApUaGUgYWJvdmUgYm94cGxvdHMgc2hvdyB0aGF0IGxpZmVzdHlsZSwgc29jbWVkLCB0ZWNoIGFuZCBvdGhlciBhcmUgdGhlIGZvdXIgY2hhbm5lbHMgdGhhdCBkcml2ZSBpbiBhdmVyYWdlIG1vcmUgc2hhcmVzIGFzIHdlIGhhdmUgc2VlbiBpbiB0aGUgZGVzY3JpcHRpb24gb2YgZGF0YS4KCiogKipDb21wYXJpc29uIGJldHdlZW4gd2Vla2RheSAmIENoYW5uZWwqKgpgYGB7cn0KZ2dwbG90KG15ZGF0YTIpICsgYWVzKHg9d2Vla2RheSxmaWxsPWNoYW5uZWwpKyBnZW9tX2Jhcihwb3NpdGlvbiA9ICJzdGFjayIpK3NjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGUgPSAiQmx1ZXMiKSt4bGFiKCJXZWVrZGF5cyIpK3lsYWIoIkZyZXF1ZW5jeSIpKyBsYWJzKHRpdGxlID0gIk92ZXJ2aWV3IGJ5IGRheSAtIEZyZXF1ZW5jeSBvZiBhcnRpY2xlIHBvc3RlZCIsIHN1YnRpdGxlID0gIkRldGFpbGVkIGJ5IGNoYW5uZWwiKSArCiAgdGhlbWUocGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSxmYWNlPSJib2xkIikscGxvdC5zdWJ0aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSkpCmBgYApXZSBtZW50aW9ubmVkIGJlZm9yZSB0aGF0IGR1cmluZyB3ZWVrZW5kLCBhcnRpY2xlcyB0aGF0IGFyZSBwb3N0ZWQgZHJpdmUgbW9yZSBzaGFyZXMuIEV2ZW4gdGhvdWdoLCB3ZSBub3RpY2UgdGhhdCB0aGVyZSBpcyBsZXNzIGFydGljbGVzIHBvc3RlZCBjb21wYXJlZCB0byBvdGhlciB3ZWVrZGF5cy4KCgoqICoqSXMgdGhlcmUgYSBsaW5rIGJldHdlZW4gc2hhcmVzIGFuZCB3b3JkIGNvbnRlbnQ/KiogIApUbyBkbyB0aGlzIHBsb3Qgd2Ugc2VsZWN0IGEgc3ViLXNhbXBsZS4KYGBge3J9CgpteWRhdGEyX3RlbXAgPSBzYW1wbGVfbih0YmwgPSBteWRhdGEyLCBzaXplID0gMTAwMCkKZ2dwbG90KG15ZGF0YTJfdGVtcCkgKyBhZXMoeD1uYl93b3Jkc19jb250ZW50LHk9bmJfc2hhcmVzKSsgZ2VvbV9wb2ludChjb2xvcj0iZGFya2JsdWUiKSArCiAgZ2VvbV9zbW9vdGgobWV0aG9kPSJsbSIpICsKICB5bGltKDAsNTAwMDApK3lsYWIoIk51bWJlciBvZiBzaGFyZXMiKSt4bGFiKCJOdW1iZXIgb2Ygd29yZHMgaW4gdGhlIGFydGljbGUiKSArIAogIGxhYnModGl0bGUgPSAiSXMgdGhlcmUgYSBsaW5rIGJldHdlZW4gc2hhcmVzIGFuZCB3b3JkIGNvbnRlbnQ/Iiwgc3VidGl0bGUgPSAiRXhjbHVkaW5nIGFydGljbGVzIHdpdGggbW9yZSB0aGFuIDUwMDAwIHNoYXJlcyIpICsKICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41LGZhY2U9ImJvbGQiKSxwbG90LnN1YnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSkKYGBgClRoZSBhYm92ZSBncmFwaCBzdWdnZXN0cyB0aGF0IHRoZSBsaW5lYXIgcmVsYXRpb24gaXMgd2Vlay4gCkhvd2V2ZXIgd2UgY2FuIHNlZSB0aGF0IG1vc3Qgb2YgdGhlIHZlcnkgc2hhcmVkIGFydGljbGVzIGRvIG5vdCBoYXZlIGEgbG90IG9mIHdvcmRzLgoKCiMjIDYuIE1hY2hpbmUgTGVhcm5pbmcgTWV0aG9kcwoKV2UgcHJvcG9zZSB0byB1c2UgdGhlIGZvbGxvd2luZyBtZXRob2RzIHRvIHByZWRpY3QgdGhlIG51bWJlciBvZiBzaGFyZXM6ICAKKioxIC0gTGluZWFyIG1vZGVsKiogIAoqKjIgLSBQZW5hbGl6ZWQgUmVncmVzc2lvbiAoTGFzc28gYW5kIFJpZGdlKSoqICAKKiozIC0gUmFuZG9tIEZvcmVzdCoqICAKCgoKV2UgY3JlYXRlIGEgY29weSBvZiB0aGF0IGRhdGFzZXQgdGhhdCB3aWxsIGJlIHVzZWQgZm9yIG1vZGVscwpgYGB7cn0KZGF0YSA8LSBteWRhdGEyCmBgYAoKCiMjIyBBZGRpdGlvbmFsIHByZWxpbWluYXJ5IGNsZWFuaW5nClRoZXJlIGFyZSBsaW5lYXIgZGVwZW5kZW5jaWVzIGluIHRoZSBkYXRhIHRoYXQgd2lsbCBwcmV2ZW50IHVzIGZyb20gcnVubmluZyBzb21lIG1vZGVscyA6ICAKcmF0ZV9wb3NpdGl2ZV93b3JkcyArIHJhdGVfbmVnYXRpdmVfd29yZHMgPSAxCmBgYHtyfQpkYXRhICU+JSBtdXRhdGUod29yZF9mZWVsaW5nID0gcmF0ZV9wb3NpdGl2ZV93b3JkcyArIHJhdGVfbmVnYXRpdmVfd29yZHMpICU+JSBnZ3Bsb3QoKSArIGFlcyh4ID0gd29yZF9mZWVsaW5nKSArIGdlb21fZGVuc2l0eSgpICsgeWxhYigiRGVuc2l0eSIpK3hsYWIoIk51bWJlciBvZiB3b3JkcyBpbiB0aGUgYXJ0aWNsZXMiKSArIAogIGxhYnModGl0bGUgPSAiRGVuc2l0eSBvZiB0aGUgc3VtIG9mIHRoZSB2YXJpYWJsZSB3b3JkX2ZlZWxpbmciKSArICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41LGZhY2U9ImJvbGQiKSkKYGBgCmxkYV8wMCArIGxkYV8wMSArIGxkYV8wMiArIGxkYV8wMyArIGxkYV8wNCA9IDEKYGBge3J9CmRhdGEgJT4lIG11dGF0ZShzdW1fbGRhID0gbGRhXzAwICsgbGRhXzAxICsgbGRhXzAyICsgbGRhXzAzICsgbGRhXzA0KSAlPiUgZ2dwbG90KCkgKyBhZXMoeCA9IHN1bV9sZGEpICsgZ2VvbV9kZW5zaXR5KCkgKyB5bGFiKCJEZW5zaXR5IikreGxhYigiTnVtYmVyIG9mIHdvcmRzIGluIHRoZSBhcnRpY2xlcyIpICsgCiAgbGFicyh0aXRsZSA9ICJEZW5zaXR5IG9mIHRoZSBzdW0gb2YgdGhlIHZhcmlhYmxlIHdvcmRfZmVlbGluZyIpICsgIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUsZmFjZT0iYm9sZCIpKQpzdW1tYXJ5KGRhdGEkbGRhXzAwICsgZGF0YSRsZGFfMDEgKyBkYXRhJGxkYV8wMiArIGRhdGEkbGRhXzAzICsgZGF0YSRsZGFfMDQpCmBgYApJbiBhYnNjaXNzLCB3ZSBoYXZlIHZhbHVlcyBvZiAxIGR1ZSB0byBlcnJvciBhcHByb3hpbWF0aW9uIGluIHRoZSBzdW0uIElmIHdlIGhhZCB0cnVuY2F0ZWQgdGhlIHZhbHVlcywgaXQgd291bGQgaGF2ZSBiZWVuIG9ubHkgYSBwZWFrIGF0IDEuICAKV2UgZXhjbHVkZSB0aGUgdHdvIHByb2JsZW1hdGljIHZhcmlhYmxlcy4KYGBge3J9CmRhdGEgPC0gZGF0YSAlPiUgc2VsZWN0KC1jKGxkYV8wNCxyYXRlX25lZ2F0aXZlX3dvcmRzKSkKYGBgCgoKIyMjIExpbmVhciBNb2RlbAoKV2Ugd2lsbCB0cnkgdG8gcHJlZGljdCB0aGUgbnVtYmVyIG9mIHNoYXJlcyBiYXNlZCBvbiBhbGwgb3RoZXIgcmVncmVzc29ycwpXZSBzdGFydCBuYWl2ZWx5IGJ5IHRyeWluZyB0byBmaXQgYSBMaW5lYXIgUmVncmVzc2lvbiB0byB0aGUgd2hvbGUgZGF0YXNldApgYGB7cn0KIyBtb2RlbC5sbSA8LSBsbShuYl9zaGFyZXN+LixkYXRhPWRhdGEpCiMgRXJyb3I6IGNhbm5vdCBhbGxvY2F0ZSB2ZWN0b3Igb2Ygc2l6ZSAxMS4wIEdiCmBgYAoKUiBjYW5ub3QgY29tcHV0ZSBpdCwgZHVlIHRvIHRoZSBleGNlc3Mgb2YgcGFyYW1ldGVycwpXZSB3aWxsIHRoZXJlZm9yZSA6IAotIHdvcmsgb25seSBvbiBhIHNhbXBsZSBvZiB0aGUgZGF0YXNldAotIGNvbnNpZGVyIG51bWVyaWNhbCB2YWx1ZXMgb25seQpgYGB7cn0KTmFtZV9udW1lcmljX2NvbHVtcyA8LSByb3duYW1lcyhkYXRhLmZyYW1lKGNvbHVtbnNfbnVtZXJpY1tjb2x1bW5zX251bWVyaWNdKSkKCk5hbWVfbnVtZXJpY19jb2x1bXMgPC0gTmFtZV9udW1lcmljX2NvbHVtc1tOYW1lX251bWVyaWNfY29sdW1zICE9ICJsZGFfMDQiICYgTmFtZV9udW1lcmljX2NvbHVtcyAhPSAicmF0ZV9uZWdhdGl2ZV93b3JkcyJdCiAgCnNldC5zZWVkKDEyMzQ1NikKZGF0YSA8LSBkYXRhICU+JSBzYW1wbGVfbigxMDAwMCkgJT4lIHNlbGVjdChOYW1lX251bWVyaWNfY29sdW1zKQpgYGAKCldlIGNhbiBub3cgc2VwYXJhdGUgaW4gdHJhaW5pbmcgYW5kIHRlc3RpbmcgZGF0YXNldHMKYGBge3J9CmRhdGEudHJhaW4gPC0gZGF0YSAlPiUgc2xpY2UoMTo4MDAwKQpkYXRhLnRlc3QgPC0gZGF0YSAlPiUgc2xpY2UoODAwMToxMDAwMCkKYGBgCgpXZSBjYW4gbm93IHJ1biBhIG5haXZlIHByZWRpY3Rpb24gbW9kZWwgKHRoYXQgcHJlZGljdHMgdGhlIHNhbXBsZSBtZWFuIG9mIHRoZSB0YXJnZXQgY29sdW1uIGluIHRoZSB0cmFpbiBkYXRhKSBhbmQgYSBsaW5lYXIgbW9kZWwKYGBge3J9CnByZWQubmFpdmUgPC0gcmVwKG1lYW4oZGF0YS50cmFpbiRuYl9zaGFyZXMpLG5yb3coZGF0YS50ZXN0KSkKbW9kZWwubG0gPC0gbG0obmJfc2hhcmVzfi4sZGF0YT1kYXRhLnRyYWluKQpgYGAKCldlIGRvIGEgc3Vic2V0IHNlbGVjdGlvbi4gV2Ugc2VsZWN0IHRoZSAqKkJJQyoqIGNyaXRlcmlhIHJhdGhlciB0aGFuIHRoZSAqKkFJQyoqIGNyaXRlcmlhIGJlY2F1c2UgaXQgcGVuYWxpc2VzIG1vcmUgYWdncmVzc2l2ZWx5IHRoZSBsYXJnZSBtb2RlbHMuIEluZGVlZCwgY29tcGFyZWQgdG8gKipBSUMqKiwgKipCSUMqKiBpbmNyZWFzZXMgYnkgJGxvZyhuKWQkIHdoZXJlYXMgKipBSUMqKiBpbmNyZWFzZXMgYnkgJDJkJC4gRm9yIG4gPSAxMDAwMCwgd2Ugd2lsbCBoYXZlICAkbG9nKG4pZCBcYXBwcm94OS4yMSQgd2hpY2ggaXMgc3VwZXJpb3IgdG8gJDIkIG9mIHRoZSAqKkFJQyoqLgoKYGBge3J9CnJlZy5maXQuZm9yd2FyZCA8LSByZWdzdWJzZXRzKG5iX3NoYXJlc34uLGRhdGE9ZGF0YS50cmFpbixtZXRob2Q9ImZvcndhcmQiLG52bWF4PTE1MCkKcmVnLmZpdC5iYWNrd2FyZCA8LSByZWdzdWJzZXRzKG5iX3NoYXJlc34uLGRhdGE9ZGF0YS50cmFpbixtZXRob2Q9ImJhY2t3YXJkIixudm1heD0xNTApCgpzdW0ucmVnLmZpdC5mIDwtIHN1bW1hcnkocmVnLmZpdC5mb3J3YXJkKQpzdW0ucmVnLmZpdC5iIDwtIHN1bW1hcnkocmVnLmZpdC5iYWNrd2FyZCkKCm5iLmJpYy5mIDwtIG9yZGVyKHN1bS5yZWcuZml0LmYkYmljKVsxXQpuYi5iaWMuYiA8LSBvcmRlcihzdW0ucmVnLmZpdC5iJGJpYylbMV0KCmdldC5mb3JtdWxhPC0gZnVuY3Rpb24oYSxudW1iZXIpewp2YXIuc2VsIDwtIGEkd2hpY2hbbnVtYmVyLF1bLTFdCnZhci5zZWwxIDwtIG5hbWVzKHZhci5zZWwpW3Zhci5zZWxdICU+JSBwYXN0ZShjb2xsYXBzZT0iKyIpCmZvcm0gPC0gZm9ybXVsYShwYXN0ZSgibmJfc2hhcmVzfiIsdmFyLnNlbDEsc2VwPSIiKSkKc3RyX2Zvcm0gPC0gcGFzdGUoIm5iX3NoYXJlc34iLHZhci5zZWwxLHNlcD0iIikKc3RyX2Zvcm0KfQoKcGxvdChyZWcuZml0LmZvcndhcmQsc2NhbGU9ImJpYyIsbWFpbj0iRm9yd2FyZCBCSUMiKQpwbG90KHJlZy5maXQuYmFja3dhcmQsc2NhbGU9ImJpYyIsbWFpbj0iQmFja3dhcmQgQklDICIpCgpzdWJzZXQubW9kZWxzIDwtIGRhdGEuZnJhbWUoQ3JpdGVyaWE9cmVwKGMoIkJpYyIpLGVhY2g9MiksTWV0aG9kPWMoIkZvcndhcmQiLCJCYWNrd2FyZCIpLEZvcm11bGEgPSBjKGdldC5mb3JtdWxhKHN1bS5yZWcuZml0LmYsbmIuYmljLmYpLGdldC5mb3JtdWxhKHN1bS5yZWcuZml0LmIsbmIuYmljLmIpKSkKCnN1YnNldC5tb2RlbHMKYGBgCldlIHNlZSB0aGF0IHRoZSB2YXJpYWJsZXMgc2VsZWN0ZWQgd2l0aCB0aGUgZm9yd2FyZCBvciBiYWNrd2FyZCBhcHByb2FjaGVzIGFyZSBhcHByb3hpbWF0ZWx5IHRoZSBzYW1lLiAgCioqRm9yIHRoZSBmb3J3YXJkIGFwcHJvY2gsIHRoZSBCSUMgY3JpdGVyaWEgc2VsZWN0cyB0aGUgbW9kZWw6KioKJFkgPSBcYmV0YV8wICsgbmIubGlua3MqXGJldGFfe25iLmxpbmtzfStrdy5tYXgubWF4KlxiZXRhX3trdy5tYXgubWF4fStrdy5taW4uYXZnKlxiZXRhX3trdy5taW4uYXZnfSQgIAokK2t3Lm1heC5hdmcqXGJldGFfe2t3Lm1heC5hdmd9K2t3LmF2Zy5hdmcqXGJldGFfe2t3LmF2Zy5hdmd9K3NlbGYucmVmZXJlbmNlLm1heC5zaGFyZXMqXGJldGFfe3NlbGYucmVmZXJlbmNlLm1heC5zaGFyZXN9K2xkYS4wMypcYmV0YV97bGRhLjAzfSQKCgoKV2UgZXZhbHVhdGUgdGhvc2UgbW9kZWxzIGJhc2VkIG9uIHRoZSBSTVNFCmBgYHtyfQpSTVNFLmxpc3QgPC0gYygpClJNU0UubGlzdCA8LSBhcHBlbmQoUk1TRS5saXN0LCBtZWFuKChwcmVkLm5haXZlLWRhdGEudGVzdCRuYl9zaGFyZXMpXjIpXigxLzIpKQpSTVNFLmxpc3QgPC0gYXBwZW5kKFJNU0UubGlzdCwgbWVhbigocHJlZGljdChtb2RlbC5sbSwgbmV3ZGF0YT1kYXRhLnRlc3QpLWRhdGEudGVzdCRuYl9zaGFyZXMpXjIpXigxLzIpKQoKZm9yIChmb3JtdWxhIGluIHN1YnNldC5tb2RlbHMkRm9ybXVsYSkgewogIG1vZGVsLnN1YiA8LSBsbShmb3JtdWxhLGRhdGE9ZGF0YS50cmFpbikgCiAgcHJlZCA8LSBwcmVkaWN0KG1vZGVsLnN1YiwgbmV3ZGF0YSA9IGRhdGEudGVzdCkgCiAgUk1TRSA8LSBtZWFuKChwcmVkLWRhdGEudGVzdCRuYl9zaGFyZXMpXjIpXigxLzIpIAogIFJNU0UubGlzdCA8LSBhcHBlbmQoUk1TRS5saXN0LFJNU0UpIAogIH0gCgpSTVNFLmNvbXBhcmlzb24gPC0gZGF0YS5mcmFtZShDcml0ZXJpYT0ibWVhbiIsTWV0aG9kPSJOYWl2ZSIsRm9ybXVsYT0iWSIpIApSTVNFLmNvbXBhcmlzb24gPC0gcmJpbmQoUk1TRS5jb21wYXJpc29uLGRhdGEuZnJhbWUoQ3JpdGVyaWE9IkxNIixNZXRob2Q9IkxNIixGb3JtdWxhPSJZIiksc3Vic2V0Lm1vZGVscykgClJNU0UuY29tcGFyaXNvbiA8LSBSTVNFLmNvbXBhcmlzb24gJT4lIHNlbGVjdChDcml0ZXJpYSxNZXRob2QpICU+JSBtdXRhdGUoUk1TRT1STVNFLmxpc3QpIAoKUk1TRS5jb21wYXJpc29uCmBgYAoKIyMjIFBlbmFsaXplZCByZWdyZXNzaW9uCmBgYHtyfQpkYXRhLmdsbW5ldCA8LSBtb2RlbC5tYXRyaXgobmJfc2hhcmVzfi4sZGF0YT1kYXRhLnRyYWluKQoKbW9kZWwucmlkZ2UgPC0gZ2xtbmV0KGRhdGEuZ2xtbmV0LGRhdGEudHJhaW4kbmJfc2hhcmVzLGFscGhhPTApCm1vZGVsLmxhc3NvIDwtIGdsbW5ldChkYXRhLmdsbW5ldCxkYXRhLnRyYWluJG5iX3NoYXJlcyxhbHBoYT0xKQoKcGxvdChtb2RlbC5yaWRnZSkKcGxvdChtb2RlbC5sYXNzbykKYGBgCldlIGNhbiBvYnNlcnZlIHRoZSByZWd1bGFyaXphdGlvbiBwYXRoIGZvciBMYXNzbyBhbmQgUmlkZ2UgUmVncmVzc2lvbi4gRm9yIExhc3NvLCBzb21lICRcYmV0YT0wJCBmb3Igc21hbGwgJHQkLgoKCmBgYHtyfQpyaWRnZUNWIDwtIGN2LmdsbW5ldChkYXRhLmdsbW5ldCxkYXRhLnRyYWluJG5iX3NoYXJlcyxsYW1iZGE9ZXhwKHNlcSgtMjAsODAsbGVuZ3RoPTMwMCkpLGFscGhhPTApCnBsb3QocmlkZ2VDVikKYGBgCgpgYGB7cn0KbGFzc29DViA8LSBjdi5nbG1uZXQoZGF0YS5nbG1uZXQsZGF0YS50cmFpbiRuYl9zaGFyZXMsbGFtYmRhPWV4cChzZXEoLTEwLDIwLGxlbmd0aD0zMDApKSxhbHBoYT0xKQpwbG90KGxhc3NvQ1YpCmBgYAoKV2UgbG9vayBhdCB0aGUgMiBiZXN0IG1vZGVscwpgYGB7cn0KbW9kZWwucmlkZ2UgPC0gZ2xtbmV0KGRhdGEuZ2xtbmV0LGRhdGEudHJhaW4kbmJfc2hhcmVzLGxhbWJkYT1yaWRnZUNWJGxhbWJkYS5taW4sYWxwaGE9MCkKbW9kZWwubGFzc28gPC0gZ2xtbmV0KGRhdGEuZ2xtbmV0LGRhdGEudHJhaW4kbmJfc2hhcmVzLGxhbWJkYT1sYXNzb0NWJGxhbWJkYS5taW4sYWxwaGE9MSkKClJNU0UucmlkZ2UgPC0gbWVhbigocHJlZGljdChtb2RlbC5yaWRnZSwgbmV3eD1tb2RlbC5tYXRyaXgobmJfc2hhcmVzfi4sZGF0YT1kYXRhLnRlc3QpKS1kYXRhLnRlc3QkbmJfc2hhcmVzKV4yKV4oMS8yKQpSTVNFLmxhc3NvIDwtIG1lYW4oKHByZWRpY3QobW9kZWwubGFzc28sIG5ld3g9bW9kZWwubWF0cml4KG5iX3NoYXJlc34uLGRhdGE9ZGF0YS50ZXN0KSktZGF0YS50ZXN0JG5iX3NoYXJlcyleMileKDEvMikKClJNU0UucGVuYWxpemVkIDwtIGRhdGEuZnJhbWUoQ3JpdGVyaWE9cGFzdGUoIkxhbWJkYSA9ICIscmlkZ2VDViRsYW1iZGEubWluKSxNZXRob2Q9IlJpZGdlIixSTVNFPVJNU0UucmlkZ2UpClJNU0UucGVuYWxpemVkIDwtIHJiaW5kKFJNU0UucGVuYWxpemVkLCBkYXRhLmZyYW1lKENyaXRlcmlhPXBhc3RlKCJMYW1iZGEgPSAiLGxhc3NvQ1YkbGFtYmRhLm1pbiksTWV0aG9kPSJMYXNzbyIsUk1TRT1STVNFLmxhc3NvKSkKClJNU0UuY29tcGFyaXNvbiA8LSByYmluZChSTVNFLmNvbXBhcmlzb24sIFJNU0UucGVuYWxpemVkKQpSTVNFLmNvbXBhcmlzb24KYGBgCgojIyMgUmFuZG9tIEZvcmVzdHMKV2UgcmVkdWNlIHRoZSBudW1iZXIgYW5kIHNpemUgb2YgdHJlZSB0byBtYWtlIGl0IGNvbXB1dGUgaW4gYSB0aW1lbHkgbWFubmVyCmBgYHtyfQptb2RlbC5yZiA8LSByYW5kb21Gb3Jlc3QobmJfc2hhcmVzfi4sZGF0YT1kYXRhLnRyYWluLG5vZGVzaXplPTMwLG50cmVlPTMwMCkKbW9kZWwucmYKYGBgCgpgYGB7cn0KUk1TRS5yZiA8LSBtZWFuKChwcmVkaWN0KG1vZGVsLnJmLCBuZXdkYXRhPWRhdGEudGVzdCktZGF0YS50ZXN0JG5iX3NoYXJlcyleMileKDEvMikKUk1TRS5jb21wYXJpc29uIDwtIHJiaW5kKFJNU0UuY29tcGFyaXNvbiwgZGF0YS5mcmFtZShDcml0ZXJpYT0iTm9kZSBzaXplID0gMzAsIG50cmVlID0gMzAwIixNZXRob2Q9IlJGIixSTVNFPVJNU0UucmYpKQpSTVNFLmNvbXBhcmlzb24KYGBgCgpXZSBpbmNyZWFzZSBwYXJhbWV0ZXJzIHRvIGdldCBhIG1vcmUgYWNjdXJhdGUgcHJlZGljdGlvbgpgYGB7cn0KbW9kZWwucmYyIDwtIHJhbmRvbUZvcmVzdChuYl9zaGFyZXN+LixkYXRhPWRhdGEudHJhaW4sbm9kZXNpemU9MSxudHJlZT00MDApCgpSTVNFLnJmIDwtIG1lYW4oKHByZWRpY3QobW9kZWwucmYyLCBuZXdkYXRhPWRhdGEudGVzdCktZGF0YS50ZXN0JG5iX3NoYXJlcyleMileKDEvMikKUk1TRS5jb21wYXJpc29uIDwtIHJiaW5kKFJNU0UuY29tcGFyaXNvbiwgZGF0YS5mcmFtZShDcml0ZXJpYT0iTm9kZSBzaXplID0gMSwgbnRyZWUgPSA0MDAiLE1ldGhvZD0iUkYiLFJNU0U9Uk1TRS5yZikpClJNU0UuY29tcGFyaXNvbgpgYGAKSXQgZG9lc24ndCBpbXByb3ZlIHRoZSBwcmVkaWN0aW9uLCBxdWl0ZSB0aGUgY29udHJhcnkKCiMjIDcuIFBlcmZvcm1hbmNlcyBvZiBlYWNoIG1vZGVscwpCYXNlZCBvbiBvdXIgdGFibGUgb2YgY29tcGFyaXNvbiBvZiBSTVNFIGZvciBlYWNoIG1vZGVscywgTGFzc28gaXMgdGhlIGJlc3QgcGVyZm9ybWluZyBtb2RlbApgYGB7cn0KcHJlZCA8LSBwcmVkaWN0KG1vZGVsLmxhc3NvLCBuZXd4PW1vZGVsLm1hdHJpeChuYl9zaGFyZXN+LixkYXRhPWRhdGEudGVzdCkpCm9icyA8LSBkYXRhLnRlc3QkbmJfc2hhcmVzCnJlcyA8LSBvYnMtcHJlZAoKUHJlZGljdGlvbiA8LSBkYXRhLmZyYW1lKHByZWQsIG9icyxyZXMpCmNvbG5hbWVzKFByZWRpY3Rpb24pIDwtIGMoInByZWQiLCJvYnMiLCJyZXMiKQoKUHJlZGljdGlvbiAlPiUgZ2dwbG90KCkgKyBhZXMoeD1yZXMpICsgZ2VvbV9kZW5zaXR5KCkKYGBgCk91ciBtb2RlbCBpcyBxdWl0ZSBvcHRpbWlzdGljIChwZWFrIGJlZm9yZSAwIGluIHJlc2lkdWFsJ3MgZGlzdHJpYnV0aW9uKSwgYnV0IHN0aWxsIG1pc3NlcyB0aGUgZmV3IHZpcmFsIGFydGljbGVzIHRoYXQgZ2V0IHNoYXJlZCBhIGxvdC4gVGhlc2UgYXJlIHRoZSBvbmVzIHRoYXQgaGF2ZSBhIG1ham9yIGltcGFjdCBvbiB0aGUgcG9vciBSTVNFIHNjb3JlCgpgYGB7cn0KUHJlZGljdGlvbiA8LSBQcmVkaWN0aW9uICU+JSBtdXRhdGUocmF0aW8gPSByZXMvb2JzKQpQcmVkaWN0aW9uICU+JSBnZ3Bsb3QoKSArIGFlcyh4PXJhdGlvKSArIGdlb21fZGVuc2l0eSgpCmBgYAoKYGBge3J9ClByZWRpY3Rpb24gJT4lIGdncGxvdCgpICsgYWVzKHg9cHJlZCwgeT1vYnMsIGNvbG9yPXJlcykgKyBnZW9tX3BvaW50KCkKYGBgCldlIGNhbiBzZWUgdmVyeSBmZXcgYXJ0aWNsZXMgcmVhY2ggbW9yZSB0aGFuIDEyNTAwIHNoYXJlcy4KVGhleSBnZXQgdmVyeSBwb29yIHByZWRpY3Rpb25zIGFzIHRoZSBtb2RlbCBuZXZlciBwcmVkaWN0cyBtb3JlIHRoYW4gMTEwMDAgc2hhcmVzLCBhbmQgaGF2ZSBhIHNpZ25pZmljYW50IGltcGFjdCBvbiB0aGUgb3ZlcmFsbCBSTVNFCgpgYGB7cn0KUHJlZC50cmltIDwtIFByZWRpY3Rpb25bUHJlZGljdGlvbiRvYnMgPCAxMjUwMCxdClByZWQudHJpbSAlPiUgZ2dwbG90KCkgKyBhZXMoeD1wcmVkLCB5PW9icywgY29sb3I9cmVzKSArIGdlb21fcG9pbnQoKQpgYGAKCmBgYHtyfQptZWFuKFByZWQudHJpbSRyZXNeMileKDEvMikKYGBgCioqSWYgd2UgaGFkIG9ubHkgb2JzZXJ2YXRpb25zIHdpdGggbGVzcyB0aGFuIDEyNTAwIHNoYXJlcyBpbiB0aGUgdGVzdCBkYXRhLCB0aGUgUk1TRSBjb3VsZCBoYXZlIGhhZCBiZWVuIGhhbGZlZC4qKgpJZiBvbiB0b3Agb2YgdGhhdCB3ZSB3b3VsZCBub3QgaGF2ZSB2aXJhbCBhcnRpY2xlcyBpbiB0aGUgdHJhaW4gZGF0YSwgdGhlIFJNU0Ugd291bGQgaGF2ZSBiZWVuIGltcHJvdmVkLgpIb3dldmVyLCBiZWNhdXNlIG91ciBnb2FsIHdhcyB0byBwcmVkaWN0IHRoZSBudW1iZXIgb2Ygc2hhcmVzIGZvciBhbGwgYXJ0aWNsZXMgKGluY2x1ZGluZyB0aGUgb25lcyB3aXRoIG1vcmUgdGhhbiAxMjUwMCBzaGFyZXMpLCB3ZSBkaWQgbm90IGV4Y2x1ZGUgcG9wdWxhciBhcnRpY2xlcyBmcm9tIHRoZSBkYXRhYmFzZS4KCgojIyA4LiBDb25jbHVzaW9uClRoZSBwb3B1bGFyaXR5IG9mIGFuIGFydGljbGUgZGVwZW5kcyBvbiBtYW55IGNoYXJhY3RlcmlzdGljczogZGF5IG9mIHB1YmxpY2F0aW9uLCBsZW5ndGgsIGNoYW5uZWwsIGNvbnRlbnTigKYgVXNpbmcgdGhlc2UgY2hhcmFjdGVyaXN0aWNzIGFzIHByZWRpY3RvcnMsIHdlIGhhdmUgdHJpZWQgdG8gcHJlZGljdCB0aGUgbnVtYmVyIG9mIHNoYXJlcyBvbiBzb2NpYWwgbmV0d29ya3MgKHBvcHVsYXJpdHkpLiBUaHJvdWdob3V0IGFuYWx5c2lzIGFuZCBtb2RlbGxpbmcsIHdlIHJlYWxpc2UgdGhhdCB0aGVyZSBpc27igJl0IGEgc3RhYmxlIGFuZCBzdGFuZGFyZCByZWNpcGUgdGhhdCB3aWxsIGRldGVybWluZSB3aXRoIGEgc3Ryb25nIGFjY3VyYWN5IHRoZSBwb3B1bGFyaXR5IG9mIGFuIGFydGljbGUuIApPdXIgbW9kZWxzIGNhbiBnaXZlIGFuIGlkZWEgb2YgdGhlIHN1Y2Nlc3MgYnV0IGFzIE1hc2hhYmxlIGhhcyBwdWJsaXNoZWQgYXJ0aWNsZXMgbWFraW5nIG1ham9yIGJ1enogKG1vcmUgdGhhbiAyMCAwMDAgc2hhcmVzIGVhY2g7IG1heGltdW0gcmVhY2hlZCB3aXRoIDg0MGsgc2hhcmVzKSwgdGhpcyB0d2lzdHMgb3VyIG1vZGVscyBhbmQgbWFrZXMgdGhlbSBtb3JlIG9wdGltaXN0aWMgdGhhdCB0aGV5IHNob3VsZCBiZS4KCioqRnVydGhlcm1vcmUsIHdlIGNvdWxkIGhhdmUgdHJhbnNmb3JtZWQgdGhlIHJlZ3Jlc3Npb24gcHJvYmxlbSBpbiBhIGNsYXNzaWZpY2F0aW9uIHByb2JsZW0sIGZvciBleGFtcGxlICRuYi5zaGFyZXM+MTIwMDAkLCB0aGF0IGNvdWxkIGhhdmUgYWxsb3cgdG8gY3JlYXRlIG1vcmUgcm9idXN0IG1vZGVsIHRvIHByZWRpY3Qgd2hldGhlciBhbiBhcnRpY2xlIHdvdWxkIHR1cm4gdmlyYWwgKG1lYW5pbmcgbnVtYmVyIG9mIHNoYXJlcyBzdXBlcmlvciB0byAxMjAwMCkgb3Igbm90LiBUaGlzIG1vZGVsIGNvdWxkIHBvdGVudGlhbGx5IGJlIGNvbWJpbmVkIGFmdGVyd2FyZHMgd2l0aCB0d28gZGlmZmVyZW50IG1vZGVscyBkZWRpY2F0ZWQgdG8gcHJlZGljdGluZyBudW1iZXIgb2Ygc2hhcmVzIGZvciB2aXJhbCBhbmQgZm9yIG5vbi12aXJhbCBhcnRpY2xlcyBleGNsdXNpdmVseS4qKiAKClRvIGZpbmlzaCwgd2UgY2FuIGtlZXAgaW4gbWluZCB0aGF0IHRoZSBjbGFzc2lmaWNhdGlvbiBtb2RlbCBpcyBhIGxlYWQgdG8gZW1wb3dlciBhdXRob3JzIGluIGRlZmluaW5nIHdoYXQgZmFjdG9ycyBjb3VsZCBiZSBtb3JlIGltcGFjdGZ1bGwgdG8gcmVzdWx0IGluIG1vcmUgZW5nYWdlbWVudCBhbmQgdmlyYWxpdHkuCgoKCgoKCgoKCgoKCgoKCg==